using System;
using System.Collections.Generic;
using System.Linq;
using NHibernate;
using NHibernate.Linq;
using FluentNHibernate;
using FluentNHibernate.Mapping;
using NUnit.Framework;
using seanfoy.nascence;

namespace seanfoy.nascence.tests {
    [TestFixture]
    public class Nascence {
        [SetUp]
        public void setup() {
            rebuildDB();
        }

        [Test]
        [ExpectedException(typeof(NHibernate.StaleStateException))]
        public void motivation() {
            testTransientCascadeSave(factory => factory.OpenSession());
        }

        [Test]
        public void cascade() {
            testTransientCascadeSave(
                factory => factory.OpenSession(new NascenceInterceptor()));
        }

        void testTransientCascadeSave(Func<ISessionFactory, ISession> openSession) {
            var factory = makeSessionFactory();
            using (var s = openSession(factory)) {
                var pta = makeMovieGraph();
                s.Save(pta);
                s.Flush();
            }
        }

        /// <summary>
        /// Use of the NascenceInterceptor doesn't impair
        /// normal NHibernate updates.
        /// </summary>
        [Test]
        public void updatesOK() {
            var factory = makeSessionFactory();
            using (var s = factory.OpenSession(new NascenceInterceptor())) {
                var review =
                    Enumerable.First(
                        s.Linq<Review>(),
                        r => "Def American".Equals(r.author));
                review.text = review.text + " (via Netflix)";
                s.Flush();
            }
            using (var s = factory.OpenSession()) {
                Assert.IsTrue(
                    Enumerable.Any(
                        s.Linq<Review>(),
                        r => r.text.EndsWith("(via Netflix)")));
            }
        }

        [Test]
        public void heavyUsageBaseline() {
            heavyUsage(factory => factory.OpenSession());
        }

        [Test]
        public void heavyUsageNascent() {
            heavyUsage(factory => factory.OpenSession(new NascenceInterceptor()));
        }

        [Test]
        public void FullGCIsnt() {
            var r = new Reprobate();
            var wr = new WeakReference(r);
            Assert.IsTrue(wr.IsAlive);
            r = null;
            GC.Collect();
            Assert.IsTrue(wr.IsAlive, "I guess this is only true for mono: weak-referenced objects aren't always collected in a 'full' collection.");
            // with unbounded new, un-weak-referenced objects,
            // the wr can survive forever. So let's open the
            // fountain of youth to an unbounded number of
            // of newbies in order to force matters to a head.
            var fountainOfYouth = new List<WeakReference>();
            while (wr.IsAlive) {
                for (int i = 0; i < 1024 * 1024; ++i) {
                    fountainOfYouth.Add(new WeakReference(new Reprobate()));
                }
            }
        }

        [Test]
        public void degenerateHashCodes() {
            var ni = new NascenceInterceptor();
            var tc = 256;
            var jetsam =
                Enumerable.Select(
                    Enumerable.Range(0, tc),
                    _ => new Reprobate()).ToList();
            Assert.AreEqual(0, oracleSize(ni));
            foreach (var r in jetsam) {
                ni.RegisterExtant(r);
            }
            var longlived = new Reprobate();
            ni.RegisterExtant(longlived);
            Assert.IsFalse(ni.NascentP(longlived));
            Assert.IsTrue(tc < oracleSize(ni));
            var sentinel = new WeakReference(jetsam[0]);
            jetsam.Clear();
            var fountain = new List<WeakReference>();
            while (sentinel.IsAlive) {
                fountain.Add(new WeakReference(new Reprobate()));
            }
            Assert.IsTrue(tc < oracleSize(ni), "This test is invalid if it can't predict when the interceptor will purge dead cache lines.");
            Assert.IsFalse(ni.NascentP(longlived), "We lost track of longlived.");
            ni.RegisterExtant(longlived);
            // I'm not sure why oracleSize != 1 by now, but
            // it's (at least sometimes) 2 on mono with
            // distinct entries, and I'd rather focus on
            // the behavior of my code than the GC so I
            // won't investigate further.
            Assert.IsTrue(oracleSize(ni) < tc, "This test expects that the interceptor purges on RegisterExtant.");
        }

        Dictionary<Int32, List<WeakReference>> oracleContents(NascenceInterceptor ni) {
            var extantField = ni.GetType().GetField("extant", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
            return (Dictionary<Int32, List<WeakReference>>)extantField.GetValue(ni);
        }

        Int32 oracleSize(NascenceInterceptor ni) {
            var extant = oracleContents(ni);
            return
                Enumerable.Sum(
                    Enumerable.Select(
                        extant.Values,
                        l => l.Count));
        }

        public void heavyUsage(Func<ISessionFactory, ISession> openSession) {
            var factory = makeSessionFactory();
            using (var s = openSession(factory)) {
                // I've profiled in dottrace with range sizes
                // 2, 1024, 16384, 32768, 65536, 1048576
                // and found that (as expected) memory usage
                // is O(1) with x_0 < 32768.
                // Also as expected, time complexity is linear.
                // The differences between NasceneInterceptor
                // and ordinary NH behavior are small, with
                // neither strategy strictly dominating the
                // other.
                foreach (int i in Enumerable.Range(0, 1024)) {
                    var review =
                        new Review() {
                            author = i.ToString(),
                            text = "it's good if I do say so myself!",
                            subject = 
                                new Movie() {
                                    title = i.ToString(),
                                    premiere = new DateTime(2009, 7, 18),
                                    cast = new [] {
                                        new Actor() {
                                            name = i.ToString(),
                                            dob = new DateTime(2009, 7, 18)}}}};
                    // I don't know how to specify a bidirectional cascade
                    // such that NHibernate will automatically traverse
                    // the subgraph it needs to be saved and then save
                    // all the transient nodes it has encountered in
                    // topographical order.
                    s.Save(review.subject);
                    s.Save(review);
                    // see https://www.hibernate.org/hib_docs/nhibernate/html/performance.html#performance-sessioncache
                    if (i % 64 == 0) {
                        s.Flush();
                        s.Clear();
                    }
                }
                s.Flush();
                s.Clear();
            }
        }

        /// <summary>
        /// Construct a transient object graph
        /// that includes a cascading edge to
        /// an entity with an assigned (e.g.,
        /// natural) key.
        /// </summary>
        /// <remarks>
        /// MovieMap cascades to reviews, but by
        /// default NHibernate has no way to
        /// determine whether the reviews are
        /// transient or not. We could give it
        /// a hint by invoking the appropriate
        /// method (e.g., Create) with a Review
        /// argument, but failing that NH may
        /// guess incorrectly.
        /// </remarks>
        private Movie makeMovieGraph() {
            var pta =
                new Movie() {
                    title = "Planes, Trains and Automobiles",
                    premiere = new DateTime(1987, 11, 25),
                    cast = new [] {
                        new Actor() {
                            name = "Steve Martin",
                            dob = new DateTime(1945, 8, 14)},
                        new Actor() {
                            name = "John Candy",
                            dob = new DateTime(1994, 3, 4)},
                        new Actor() {
                            name = "Kevin Bacon",
                            dob = new DateTime(1958, 7, 8)}},
                        reviews = new [] {
                            new Review() {
                            author = "Luke Y. Thompson",
                            text = "John Hughes at the peak of his powers, with two masterful comedians at theirs."}}};
            foreach (var r in pta.reviews) {
                r.subject = pta;
            }
            return pta;
        }

        static FluentNHibernate.Cfg.FluentConfiguration configureDB() {
            return
                FluentNHibernate.Cfg.Fluently.Configure(
                    new NHibernate.Cfg.Configuration()).
                Mappings(
                    m => m.FluentMappings.AddFromAssemblyOf<MovieMap>());
        }

        static ISessionFactory makeSessionFactory() {
            return configureDB().BuildSessionFactory();
        }
        static void buildSchema(NHibernate.Cfg.Configuration config) {
            new NHibernate.Tool.hbm2ddl.SchemaExport(config).
                SetOutputFile("../ddl.sql").
                Create(false, true);
        }
        static void rebuildDB() {
            using (var factory =
                   configureDB().
                   ExposeConfiguration(buildSchema).
                   BuildSessionFactory()) {
                using (var s = factory.OpenSession()) {
                    var redford = new Actor() { name = "Robert Redford", dob = new DateTime(1936, 8, 18) };
                    var poitier = new Actor() { name = "Sidney Poitier", dob = new DateTime(1927, 2, 20) };
                    var strathairn = new Actor() { name = "David Strathairn", dob = new DateTime(1949, 1, 26) };
                    var aykroyd = new Actor() { name = "Dan Aykroyd", dob = new DateTime(1952, 7, 1) };
                    var phoenix = new Actor() { name = "River Phoenix", dob = new DateTime(1970, 8, 23) };
                    var mcdonnell = new Actor() { name = "Mary McDonnell", dob = new DateTime(1952, 4, 28) };
                    var streep = new Actor() { name = "Meryl Streep", dob = new DateTime(1949, 6, 22) };
                    foreach (var a in new [] {redford, poitier, strathairn, aykroyd, phoenix, mcdonnell, streep}) {
                        s.Save(a);
                    }
                    var sneakers =
                        new Movie() {
                            title = "Sneakers",
                            premiere = new DateTime(1992, 9, 9),
                            cast = new [] {
                                redford,
                                poitier,
                                strathairn,
                                aykroyd,
                                phoenix,
                                mcdonnell}};
                    s.Save(sneakers);
                    var ooa =
                        new Movie() {
                            title = "Out of Africa",
                            premiere = new DateTime(1985, 12, 18),
                            cast = new [] {
                                streep,
                                redford}};
                    s.Save(ooa);
                    s.Save(
                        new Review() {
                            subject = sneakers,
                            author = "Rob Thomas",
                            text = "Great caper comedy that deftly balances high-tech gadgets and character humor."});
                    s.Save(
                           new Review() {
                               subject = ooa,
                               author = "Marjorie Baumgarten",
                               text = "A sumptuous romantic drama, Out of Africa spans continents and partners in while relating the tale of how Karen Blixen grew to become the world-famous author."});
                    var ford =
                        new Actor() {
                            name = "Harrison Ford",
                            dob = new DateTime(1942, 6, 13)};
                    var mirren =
                        new Actor() {
                            name = "Helen Mirren",
                            dob = new DateTime(1945, 6, 26)};
                    var moscoast =
                        new Movie() {
                            title = "The Mosquito Coast",
                            premiere = new DateTime(1986, 11, 26),
                            cast = new [] {
                                ford,
                                mirren,
                                phoenix}};
                    s.Save(moscoast);
                    s.Save(
                        new Review() {
                            subject = moscoast,
                            author = "Def American",
                            text = "This is an unacknowledged great film. Peter Weir and Harrison Ford reunited after \"Witness\",and managed to make back to back classics. Paul Shrader(\"Taxi Driver\",\"Raging Bull\") contributes his typical brilliant screen play. River Phoenix was a bright,shining star. He gives this film it's soul,and matches Ford with a tour-de-force performance. This truly was Weir and Ford at their best. Unfortunatly, this was lost on both audiences and critics. To this day it is the only Ford film to lose money-and pushed a shattered Weir into the safe arms of \"The Dead Poet's Society\". Ford himself would never again take a risky or edgy role. Revisit this film. It is a dark and brilliant masterpiece."});
                    s.Flush();
                }
            }
        }
    }

    public class Movie {
        public virtual String title { get; set; }
        public virtual DateTime premiere { get; set; }
        public virtual IEnumerable<Review> reviews { get; set; }
        public virtual IEnumerable<Actor> cast { get; set; }
    }

    public class Review {
        public virtual Movie subject { get; set; }
        public virtual String author { get; set; }
        public virtual String text { get; set; }

        private Dictionary<String, Object> getValueEquality() {
            return new Dictionary<String, Object>() {
                {"subject", subject},
                {"author", author}};
        }
        override public Boolean Equals(object o) {
            Review other = o as Review;
            if (Object.ReferenceEquals(other, null)) return false;
            return
                Enumerable.All(
                    Enumerable.Join(
                        getValueEquality(),
                        other.getValueEquality(),
                        p => p.Value,
                        p => p.Value,
                        (p, q) => new {p = p, q = q}),
                    p => p.p.Equals(p.q));
        }
        override public Int32 GetHashCode() {
            return
                Enumerable.Aggregate(
                    Enumerable.Select(
                        getValueEquality(),
                        (p, j) => new {i = p.Value.GetHashCode(), j = j}),
                    0,
                    (a, p) => {
                        unchecked {
                            return a + p.i * p.j;
                        }});
        }
    }

    public class Actor {
        public virtual String name { get; set; }
        public virtual DateTime dob { get; set; }
    }

    public class MovieMap : ClassMap<Movie> {
        public MovieMap() {
            Id(x => x.title);
            Map(x => x.premiere);
            HasMany<Review>(x => x.reviews)
                .Cascade.All().
                KeyColumns.Add("subject");
        }
    }
    public class ReviewMap : ClassMap<Review> {
        public ReviewMap() {
            CompositeId().
                KeyReference(x => x.subject).
                KeyProperty(x => x.author);
            Map(x => x.text).Length(1048576);
        }
    }
    public class ActorMap : ClassMap<Actor> {
        public ActorMap() {
            Id(x => x.name);
            Map(x => x.dob);
        }
    }
    public class Reprobate {
        override public Boolean Equals(Object other) {
            return Object.ReferenceEquals(this, other);
        }
        override public Int32 GetHashCode() {
            return 1;
        }
        override public String ToString() {
            return String.Format("Reprobate@{0}", base.GetHashCode());
        }
    }
}
